/*
* Copyright 2010 Pavel Kozhin 
*
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed  under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License.
*
* Author: Pavel Kozhin.
*/

package org.bookmarksframework.core
{
	import flash.events.Event;
	import flash.events.IEventDispatcher;
	import flash.utils.Dictionary;
	
	import org.bookmarksframework.events.FragmentCommitEvent;
	import org.bookmarksframework.events.FragmentEvent;
	import org.bookmarksframework.rule.IRuleParser;
	import org.bookmarksframework.util.CallLater;
	
	[ExcludeClass]
	public class Committer implements ICommitter
	{
		protected var resumer:Function;
		
		protected var storage:IStorage;
		
		protected var dispatcher:IEventDispatcher;
		
		protected var isPending:Boolean = false;
		
		protected var counter:int = 0;
		
		protected var _isIdle:Boolean = true;

		public function get isIdle():Boolean
		{
			return _isIdle;
		}
		
		protected var _info:CommitInfo = new CommitInfo();

		public function get info():CommitInfo
		{
			return _info;
		}
		
		protected var _affectedFragments:Vector.<IFragment>;

		public function get affectedFragments():Vector.<IFragment>
		{
			return _affectedFragments;
		}
		
		protected var _path:Path;
		
		public function get path():Path
		{
			return _path;
		}
		
		private var _delay:int = 1;
		
		public function get delay():int
		{
			return _delay;
		}
		
		public function set delay(value:int):void
		{
			_delay = value;
		}
		
		public function initialize(storage:IStorage, dispatcher:IEventDispatcher):void
		{
			if(isIdle && !isPending)
			{
				this.storage = storage;
				this.dispatcher = dispatcher;
			}
		}
		
		public function commit(path:Path, fragment:IFragment = null):void
		{
			if (isIdle && !isPending)
			{
				var children:Vector.<FacileHierarchy>;
				_info.fragment = fragment;
				_info.path = path;
				_affectedFragments = new Vector.<IFragment>();
				counter = 0;
				_isIdle = false;
				
				if (fragment && storage.contains(fragment))
				{
					children = new Vector.<FacileHierarchy>();
					children.push(storage.getItem(storage.getKey(fragment)));
				}
				else
				{
					children = storage.getChildren();  
				}
				
				_path = path;
				commitHierarchy(children, path.getChildren());
				
				/*CallLater.execute2(commitHierarchy, 
					[children, path.getChildren()], delay);*/
			}
		}
		
		protected function commitHierarchy(controllers:Vector.<FacileHierarchy>, 
										   path:Vector.<FacileHierarchy>):void
		{
			var controller:IFragmentController;
			var fragment:IFragment;
			var committedEvent:FragmentCommitEvent;
			var evt:FragmentCommitEvent;
			var v1:Vector.<FacileHierarchy>;
			var v2:Vector.<FacileHierarchy>
			
			for (var i:int = 0; i < controllers.length; ++i)
			{
				if (i <= (path.length - 1))
				{
					controller = controllers[i].value as IFragmentController;
					fragment = controller.fragment;
					
					if (!fragment.active)
					{
						trace("Bookmarks Warning: Path \""+info.path.value+"\" can't " +
							"be properly committed. Check your fragments structure.");
						continue;
					}
					
					this.path.updateFragment(path[i], controller.fragment);
					
					v1 = controllers.slice(i);
					v2 = path.slice(i);
					resumer = function():void { commitHierarchy(v1, v2); };
					committedEvent = commitFragment(controller.fragment, path[i].value);
					
					if (isPending)
						return;
					
					if (committedEvent)
					{
						counter++;
						CallLater.execute2(postCommitHierarchy, 
							[path[i], controllers[i], committedEvent], 1);
					}
				}
				else
				{
					break;
				}
			}
			
			if (!counter)
			{
				CallLater.execute2(complete);
			}
		}
		
		protected function postCommitHierarchy(path:FacileHierarchy, controller:FacileHierarchy, 
											   committedEvent:FragmentCommitEvent):void
		{	
			var postCommitEvent:FragmentCommitEvent;
			
			postCommitEvent = new FragmentCommitEvent(FragmentCommitEvent.POSTCOMMIT, 
				committedEvent.fragment, committedEvent.part, committedEvent.value, 
				committedEvent.oldPart, committedEvent.oldValue, committedEvent.initiator);
			
			if (committedEvent.fragment.hasEventListener(FragmentCommitEvent.POSTCOMMIT))
				committedEvent.fragment.dispatchEvent(postCommitEvent);
			
			info.dispatchEvent(postCommitEvent.clone());
			dispatcher.dispatchEvent(postCommitEvent.clone());
			
			var pathChildren:Vector.<FacileHierarchy>;
			var controllerChildren:Vector.<FacileHierarchy>;
			pathChildren = path.getChildren();
			controllerChildren = controller.getChildren();
			
			counter--;
			
			if (controllerChildren.length > 1)
				controllerChildren = filterControllers(controllerChildren);
			
			if (pathChildren.length && 
				controllerChildren.length)
			{
				commitHierarchy(controllerChildren, pathChildren);
			}
			else
			{
				if (!counter)
				{
					CallLater.execute2(complete);
				}
			}
		}
		
		protected function fragmentPostponeHandler(event:FragmentEvent):void
		{
			info.dispatchEvent(event.clone());
			dispatcher.dispatchEvent(event.clone());
		}
		
		protected function fragmentResumeHandler(event:FragmentEvent):void
		{
			if (!IFragment(event.target).pending && (resumer != null))
			{
				event.target.removeEventListener(
					FragmentEvent.RESUME, fragmentResumeHandler);
				isPending = false;
				info.dispatchEvent(event.clone());
				dispatcher.dispatchEvent(event.clone());
				resumer();
			}
		}
		
		private function filterControllers(controllers:Vector.<FacileHierarchy>):Vector.<FacileHierarchy>
		{
			var result:Vector.<FacileHierarchy> = new Vector.<FacileHierarchy>();
			var dictionary:Dictionary = new Dictionary();
			var controllersLen:int = controllers.length;
			var controller:IFragmentController;
			var parent:IFragmentController;
			var parentCounter:int = 0;
			var byParent:Array = [];
			var byParentLen:int;
			var facileParent:FacileHierarchy;
			
			// TODO: improve
			for (var i:int = 0; i < controllersLen; ++i)
			{
				controller = controllers[i].value as IFragmentController;
				facileParent = controllers[i].getParent();
				parent = facileParent.value as IFragmentController;
				
				if (!parent)
					continue;
				
				if (dictionary[parent] == null)
				{
					dictionary[parent] = parentCounter++;
					byParent[dictionary[parent]] = new Vector.<FacileHierarchy>();
				}
				
				byParent[dictionary[parent]].push(controllers[i]);
			}
			
			byParentLen = byParent.length;
			
			for (i = 0; i < byParentLen; ++i)
			{
				if (byParent[i].length > 1)
				{
					byParent[i] = byParent[i].filter(filterActiveControllers); 
				}
			}
			
			for (i = 0; i < byParentLen; ++i)
			{
				result = result.concat(byParent[i]);
			}
			
			return result;
		}
		
		private function filterActiveControllers(item:*, index:int, 
												 array:Vector.<FacileHierarchy>):Boolean
		{
			return IFragmentController(item.value).fragment.active;
		}
		
		protected function commitFragment(fragment:IFragment, part:*):FragmentCommitEvent
		{
			var result:Boolean = false;
			var parser:IRuleParser = fragment.ruleParser;
			var isCommitPrevented:Boolean = false;
			var evt:Event;
			var parsedValue:*;
			var oldValue:*;
			var oldPart:*;
			var value:*;
			
			if (parser.validate(part))
			{
				value = parser.findValue(part + "");
				
				if (value !== null)
				{
					oldValue = fragment.value;
					oldPart = fragment.part;
					
					evt = new FragmentCommitEvent(FragmentCommitEvent.COMMITTING, 
						fragment, part, value, oldPart, oldValue, info.fragment);
					
					fragment.addEventListener(
						FragmentEvent.POSTPONE, fragmentPostponeHandler);
					
					if (fragment.hasEventListener(FragmentCommitEvent.COMMITTING))
					{
						fragment.dispatchEvent(evt);
						isCommitPrevented = evt.isDefaultPrevented();
					}
					
					fragment.removeEventListener(
						FragmentEvent.POSTPONE, fragmentPostponeHandler);
					
					if (fragment.pending)
					{
						isPending = true;
						fragment.removeEventListener(
							FragmentEvent.RESUME, fragmentResumeHandler);
						fragment.addEventListener(
							FragmentEvent.RESUME, fragmentResumeHandler);
						
						return null;
					}
					
					if (info.hasEventListener(FragmentCommitEvent.COMMITTING))
					{
						evt = evt.clone();
						info.dispatchEvent(evt);
						isCommitPrevented = isCommitPrevented || evt.isDefaultPrevented();
					}
					
					if (dispatcher.hasEventListener(FragmentCommitEvent.COMMITTING))
					{
						evt = evt.clone();
						dispatcher.dispatchEvent(evt);
						isCommitPrevented = isCommitPrevented || evt.isDefaultPrevented();
					}
					affectedFragments.push(fragment);
					result = !isCommitPrevented;
					
					if (result)
					{
						fragment.value = value;
						fragment.part = part;
						
						evt = new FragmentCommitEvent(FragmentCommitEvent.COMMIT, 
							fragment, part, value, oldPart, oldValue, info.fragment);
						
						if (fragment.hasEventListener(FragmentCommitEvent.COMMIT))
							fragment.dispatchEvent(evt);
						
						info.dispatchEvent(evt.clone());
						dispatcher.dispatchEvent(evt.clone());
						
						return evt as FragmentCommitEvent;
					}
				}
				else
				{
					trace("Bookmarks Warning: can't find value for part \""+part+"\" " +
						"and rule \""+fragment.rule+"\"");
				}
			}
			else
			{
				trace("Bookmarks Warning: part \""+part+"\" is not valid for rule " +
					"\""+fragment.rule+"\"");
			}
			
			return null;
		}
		
		protected function complete():void
		{
			if (!isIdle)
			{
				_isIdle = true;
				info.dispatchEvent(new Event(Event.COMPLETE));
				dispatcher.dispatchEvent(new Event(Event.COMPLETE));
			}
		}
		
	}
}